#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# vim: expandtab tabstop=4 softtabstop=4 shiftwidth=4
#
# Author: Liang Guanchao <lianggc@gmail.com>
# 2010/09/04
# 
# This module is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License V2 as published 
# by the Free Software Foundation.
# 
# Ref: http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
#
# This module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY. Use it at your own risk. Don't forget to back up
# your data!
#
#
""" SkyDriveApi

"""
import os
import os.path
import sys
import stat
import traceback
import re
import time
import copy
import urllib
import urllib2
import cookielib
import htmlentitydefs
import mimetools, mimetypes
import logging
from urlparse import urlsplit
from cStringIO import StringIO

_DEBUG = True

def get_config_path():
    """Return the directory path to store pyskydrive configuration."""
    path = os.path.expanduser("~/.pyskydrive")
    if not os.access(path, os.F_OK):
        os.mkdir(path, 0755)
    return path

def get_input_value(html, name):
    """Return the value of input element whose name is name in html."""
    re_input = re.compile(r'<input[^>]*?name=([\'"])' + name + '\1[^>]*?>', 
            re.I | re.S)
    re_value = re.compile(r'<input[^>]*?value=([\'"])(?P<value>.*?)(\1)'
            '[^>]*?>', re.I | re.S)

    m_input = re_input.search(html)
    m_value = None

    if m_input:
        m_value = re_value.search(m_input.group(0))
    if m_value: 
        return m_value.group('value')

    return ""

def generate_form_params(html, form_name):
    """Return a map of all elements of form_name in html""" 
    re_form = re.compile(r'<form.*?name=([\'"])' + form_name + 
            r'\1.*?>.*?</form>', re.I | re.S)
    re_input = re.compile(r'<input[^>]*?name=([\'"])(?P<name>.*?)\1[^>]*?>', 
            re.I | re.S)
    re_value = re.compile(r'<input[^>]*?value=([\'"])(?P<value>.*?)(\1)'
            '[^>]*?>', re.I | re.S)

    params = {}

    form_html = re_form.search(html).group(0)

    m_input = re_input.search(form_html)
    while m_input:
        name = m_input.group('name')
        m_value = re_value.search(m_input.group(0))
        if m_value:
            value = m_value.group('value')
        else:
            value = ""
        params[name] = value

        m_input = re_input.search(form_html, m_input.end())

    return params


def get_js_var_value(html, var):
    """Return javascript variable value whose name is var"""
    re_var = re.compile(r'var\s+' + var + r'\s*=\s*([\'"]?)(?P<value>.*?)(\1);')
    m_var = re_var.search(html)
    if m_var:
        return m_var.group('value')
    else:
        return ""

def get_element_attr_by_id(html, element_id, attr):
    """Return value of attr of element whose id is element_id"""
    re_elem = re.compile(r'<[^>]*?id=([\'"])' + element_id + r'(\1).*?>') 
    re_attr = re.compile(r'<[^>]*?' + attr + r'=([\'"])(?P<value>.*?)(\1).*?>') 

    m_elem = re_elem.search(html)
    if m_elem: 
        m_attr = re_attr.search(m_elem.group(0))
        if m_attr: 
            return unescape(m_attr.group('value'))

    return ""

def unescape(text):
    """Removes HTML or XML character references 
      and entities from a text string.
      keep &amp;, &gt;, &lt; in the source code.
    from Fredrik Lundh
    http://effbot.org/zone/re-sub.htm#unescape-html
    """
    def fixup(match_obj):
        """Return the unescape value of match_obj"""
        text = match_obj.group(0)
        if text[:2] == "&#":
            # character reference
            try:
                if text[:3] == "&#x":
                    return unichr(int(text[3:-1], 16))
                else:
                    return unichr(int(text[2:-1]))
            except ValueError:
                print "erreur de valeur"
        else:
            # named entity
            try:
                if text[1:-1] == "amp":
                    text = "&amp;amp;"
                elif text[1:-1] == "gt":
                    text = "&amp;gt;"
                elif text[1:-1] == "lt":
                    text = "&amp;lt;"
                else:
                    print text[1:-1]
                    text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
            except KeyError:
                print "keyerror"
        return text # leave as is
    return re.sub("&#?\w+;", fixup, text)

class Callable:
    def __init__(self, anycallable):
        self.__call__ = anycallable

# Controls how sequences are uncoded. If true, elements may be given multiple 
# values by assigning a sequence.
_DOSEQ = 1

class MultipartPostHandler(urllib2.BaseHandler):
    handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first

    def http_request(self, req):
        data = req.get_data()
        if data is not None and type(data) != str:
            v_files = []
            v_vars = []
            try:
                for(key, value) in data.items():
                    if type(value) == file:
                        v_files.append((key, value))
                    else:
                        v_vars.append((key, value))
            except TypeError:
                systype, value, tb = sys.exc_info()
                raise (TypeError, "not a valid non-string sequence or mapping "
                       "object", tb)

            if len(v_files) == 0:
                data = urllib.urlencode(v_vars, _DOSEQ)
            else:
                boundary, data = self.multipart_encode(v_vars, v_files)

                contenttype = 'multipart/form-data; boundary=%s' % boundary
                req.add_unredirected_header('Content-Type', contenttype)

            req.add_data(data)
        
        return req

    def multipart_encode(params, files, boundary = None, buf = None):
        if boundary is None:
            boundary = mimetools.choose_boundary()

        if buf is None:
            buf = StringIO()

        for(key, value) in params:
            buf.write('--%s\r\n' % boundary)
            buf.write('Content-Disposition: form-data; name="%s"' % key)
            buf.write('\r\n\r\n' + value + '\r\n')

        for(key, fd) in files:
            #file_size = os.fstat(fd.fileno())[stat.ST_SIZE]
            filename = fd.name.split('/')[-1]
            contenttype = (mimetypes.guess_type(filename)[0] or 
                    'application/octet-stream')

            buf.write('--%s\r\n' % boundary)
            buf.write('Content-Disposition: form-data; name="%s"; '
                      'filename="%s"\r\n' % (key, filename))
            buf.write('Content-Type: %s\r\n' % contenttype)
            # buffer += 'Content-Length: %s\r\n' % file_size

            fd.seek(0)
            buf.write('\r\n' + fd.read() + '\r\n')

        buf.write('--' + boundary + '--\r\n\r\n')
        buf = buf.getvalue()

        return boundary, buf

    multipart_encode = Callable(multipart_encode)

    https_request = http_request


class SkyDriveApiError(Exception):
    """ Base class for all SkyDriveApi exceptions """

    def __init__(self, msg):
        self.msg = str(msg)
        self._type, self._value, self._tb = sys.exc_info()

        Exception.__init__(self, str(msg))

    def __str__(self):
        return repr(self.msg)

    def __unicode__(self):
        return unicode(self.msg)
    
    def print_exception(self):
        """Print exception traceback."""
        traceback.print_exception(self._type, self._value, self._tb)

class SkyDriveApiInternalError(SkyDriveApiError):
    """ SkyDriveApi Internal Error
    usually means code bug or page structure changed.
    """

class SkyDriveApiAuthError(SkyDriveApiError):
    """Authorization Error (Invalid email/password) """
    pass
    
class SkyDriveApiOSError(SkyDriveApiError):
    """OS Error"""
    pass

class SkyDriveApiNetworkError(SkyDriveApiError):
    """Network Error (URLError etc.)"""
    pass

class SkyDriveApiNotExistError(SkyDriveApiError):
    """Skydrive item not exist error"""
    pass

class SkyDriveApiNoFileError(SkyDriveApiError):
    """No skydrive file error"""
    pass

class SkyDriveApiNoDirError(SkyDriveApiError):
    """No skydrive directory error"""
    pass


class SkyDriveItem:
    """Base class for SkyDrive file/directory/albumn/favorite

    base_url can be inherited from parents.
    
    """

    def __init__(self, name, owner = "", mtime = "", parent = None):
        self.name = name
        self.size = -1
        self.owner = owner
        self.mtime = mtime
        self.desc = ""
        self.parent = parent
        self.urls = {}

        self._base_url = ""
        self.timestamp = 0

    def get_abs_path(self):
        """Return absolute path of this item"""
        if self.parent is None:
            return self.name
        else:
            parent_path = self.parent.get_abs_path()
            if parent_path == "/":
                return parent_path + self.name
            else:
                return parent_path + "/" +  self.name

    def get_browse_url(self):
        """Return browse url of this item"""
        # Note: We need to encode path as utf-8, otherwise quote will fail.
        path = self.get_abs_path().encode('utf-8')
        return self.get_base_url() + "/browse.aspx" + urllib.quote(path)

    def set_base_url(self, url):
        """Set base url for this item (scheme://netloc)."""
        self._base_url = url

    def get_base_url(self):
        """Get base url for this item.

        If base url is not set, we will get from its ancestor.
        """
        if self._base_url != "": 
            return self._base_url
        elif self.name != "/" and self.parent:
            return self.parent.get_base_url()
        else:
            return ""

class SkyDriveDir(SkyDriveItem):
    """Skydrive Directory."""
    def __init__(self, name, owner = "", mtime = "", parent = None):
        self._subitems = []
        SkyDriveItem.__init__(self, name, owner, mtime, parent)

    def get_subitems(self):
        """Return the list of subitems."""
        return self._subitems

    def add_child(self, item):
        """Add item to the directory's children."""
        item.parent = self
        self._subitems.append(item)

    def del_child(self, item):
        """Remove the child item"""
        self._subitems.remove(item)

    def clear(self):
        """Remove all children of this directory."""
        del self._subitems
        self._subitems = []

    def find_child(self, name):
        """Return the child item whose name is name if there is."""
        for item in self.get_subitems():
            if item.name.lower() == name.lower():
                return item
        return None

class SkyDriveFile(SkyDriveItem):
    """SkyDrive File"""
    pass

def isdir(item):
    """Return True if item is a SkyDriveDir. Otherwise False."""
    return isinstance(item, SkyDriveDir)

def isfile(obj):
    """Return True if item is a SkyDriveFile. Otherwise False."""
    return isinstance(obj, SkyDriveFile)


_HOME_PAGE = "http://skydrive.live.com"

_LOGIN_URL = ("http://login.live.com/login.srf?wa=wsignin1.0&rpsnv=10&ct"
        "=1242028992&rver=5.5.4177.0&wp=MBI&wreply=http:%2F%2Fskydrive.live."
        "com%2Fhome.aspx%3Fmkt%3Den-us&lc=1033&id=250206&mkt=en-US")

_USER_AGENT = ('User-agent' , 'Mozilla/5.0 (Windows; U; Windows NT 6.1; '
               'zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8')

_RE_USER_NAME = re.compile(r'(?P<uid>[\w.]+)@(?P<domain>[\w.]+)')

_RE_LOGIN_PPFT = re.compile(r'<input type="hidden" name="PPFT" id="(?P<id>\w+)"'
                            r' value="(?P<value>.*?)"/>')
_RE_LOGIN_REFRESH = re.compile(r'<meta http-equiv="REFRESH" content="0; URL='
                               r'(?P<url>.*?)"/?>')

_RE_INDEX_URL = re.compile(r'<a class="tvLink".*?href="(?P<href>.*?)">')
_RE_INDEX_NAME = re.compile(r'<span class="tvItemText">(?P<name>.*?)</span>')

_STR_BROWSE_BEGIN = ""

_RE_BROWSE_ROW = re.compile(r'<a  id="(?P<id>[^"]*?)" class="dvRow"')
_RE_BROWSE_NAME = re.compile(r'<a href="(?P<href>[^"]*?)".*?class="dvName" '
                             r'title="(?P<name>.*?)".*?>.*?</a>')
_RE_BROWSE_PERSON = re.compile(r'<div class="dvPerson">(?P<person>.*?)</div>')
_RE_BROWSE_TIME = re.compile(r'<div class="dvTime">(?P<time>.*?)</div>') 
_RE_BROWSE_DEL = re.compile(r"window.execCommand\('dvDeleteItem','[^']*', "
                            r"'(?P<id>[^']*)'")
_RE_BROWSE_URL = re.compile(r'<a id="(?P<id>[^"]*)" href="(?P<href>[^"]*)"')


def _get_download_folder_url(item):
    """Return downloadFolder url for item if it's a directory."""
    if not isdir(item):
        raise SkyDriveApiNoDirError(item.name)

    sock = urllib2.urlopen(item.get_browse_url())
    html = sock.read()
    url = get_element_attr_by_id(html, "downloadFolder", "href")
    #canary = get_input_value(html, "canary")

    return url

def cookie_maker():
    """utility func for create a cookie."""
    return lambda n, v, d: cookielib.Cookie(version=0, 
            name = n, value = v, port=None, port_specified=False, domain = d, 
            domain_specified=False, domain_initial_dot=False, path='/', 
            path_specified=True, secure=False, expires=None, discard=True, 
            comment=None, comment_url=None, rest={'HttpOnly': None})
    
def _call_hook(func, *args):
    """Call the hook function if it's not None."""
    if func:
        func(*args)

class MyCookiePolicy(cookielib.DefaultCookiePolicy):
    """Cookie Policy for live.com

    With python 2.6, cookie.DefaultCookiePolicy works. But with python 2.4,
    cookie.DefaultCookiePolicy won't accept cookies whose version > 0 unless
    rfc2965 is set to true. The domain ".login.live.com" isn't accepted also.
    """
    def set_ok(self, cookie, request):
        # Note: Fixed for python 2.4
        cookie.version = 0
        if not cookielib.DefaultCookiePolicy.set_ok(self, cookie, request):
            return False

        return True

class SkyDriveAccount:

    def __init__(self, user_name):
        self._user_name = user_name
        self._auth = False

        self._root_dir = SkyDriveDir("/")
        self._root_dir.timestamp = time.time()
        self._cwd = self._root_dir

        path = self.get_cookie_path()
        policy = MyCookiePolicy(rfc2965 = True)
        self._cookie_jar = cookielib.MozillaCookieJar(path, policy = policy)

        h1 = urllib2.HTTPHandler()
        #h1.set_http_debuglevel(1)
        h2 = urllib2.HTTPSHandler()
        #h2.set_http_debuglevel(2)
        self._opener = urllib2.build_opener(h1, h2,
                urllib2.HTTPCookieProcessor(self._cookie_jar),
                urllib2.HTTPRedirectHandler) 

        self._opener.addheaders = [_USER_AGENT]

    def is_authorized(self):
        """Return if this account has logged in."""
        return self._auth

    def get_cookie_path(self):
        """Return the cookie file (absolute) path of this account."""
        config_dir = get_config_path()

        try:
            import hashlib
            name = hashlib.md5(self._user_name).hexdigest() + ".ck"
        except ImportError:
            import md5
            name = md5.new(self._user_name).hexdigest() + ".ck"
        
        return os.path.join(config_dir, name)

    def _get_html(self, url, referer = "", params = None):
        """Fetch page html of url.

        If params is None, we will get the page using http-get.
        In case params is map, http-post will be used instead.
        """
        req = urllib2.Request(url, params)
        if referer != "": 
            req.add_header('Referer', referer)

        sock = self._opener.open(req)
        return sock.read()

    def login_with_cookie(self, hook = None):
        """Verify the account by saved cookie file."""
        _call_hook(hook, 0)

        try:
            self._cookie_jar.load()

            html = self._get_html(_HOME_PAGE)
            _call_hook(hook, 50)

            m_refresh = _RE_LOGIN_REFRESH.search(html)
            if m_refresh:
                html = self._get_html(m_refresh.group('url'))

            action = get_element_attr_by_id(html, "fmHF", "action")
            params = generate_form_params(html, "fmHF")
            params = urllib.urlencode(params)

            html = self._get_html(action, "", params)
            _call_hook(hook, 100)

            self._cookie_jar.save()

            self._parse_root_page(html)
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)
        except (cookielib.LoadError, IOError), err:
            raise SkyDriveApiInternalError(err)
        else:
            self._auth = True

    def login_with_passwd(self, passwd, hook = None):
        """Verify the account by given password."""
        _call_hook(hook, 0)

        try:
            # Round 1
            html = self._get_html(_LOGIN_URL)
            _call_hook(hook, 25)

            ppft_val =  _RE_LOGIN_PPFT.search(html).group('value')
            postback_url = get_js_var_value(html, "srf_uPost")

            msecs = int(time.time() * 1000)
            match_user_name = _RE_USER_NAME.match(self._user_name)
            make_cookie = cookie_maker()

            self._cookie_jar.set_cookie(make_cookie("CkTst", "G%d" % msecs, 
                "login.live.com"))
            self._cookie_jar.set_cookie(make_cookie("wlidperf", 
                    "throughput=7&latency=421&FR=L&ST=%d" % msecs, ".live.com"))

            self._cookie_jar.set_cookie(
                    make_cookie("WLopt", "WLopt=RDCache=@%s:4&nrme=1" % 
                        match_user_name.group('domain'), "login.live.com")
                    )
            self._cookie_jar.set_cookie(make_cookie("wl_preperf", 
                    "plt=2&req=15&com=15&cache=10", ".live.com"))

            params = {
                      "i1" : "", 
                      "i2" : "1", 
                      "i3" : "11564", 
                      "i4" : "", 
                      "i12" : "",
                      "idsbho" : "1",
                      "login" : self._user_name,
                      "LoginOptions" : "1",
                      "MEST" : "",
                      "NewUser" : "1",
                      "passwd" : passwd,
                      "PPFT" : ppft_val,
                      "PPSX" : get_js_var_value(html, "srf_sRBlob"),
                      "PwdPad" : "",
                      "sso" : "",
                      "type" : "11"
                     }
            params = urllib.urlencode(params)

            # Round 2
            html = self._get_html(postback_url, _LOGIN_URL, params)
            _call_hook(hook, 50)

            err_msg = get_js_var_value(html, "srf_sErr")
            if err_msg != "": 
                raise SkyDriveApiAuthError(err_msg)

            refresh_url = _RE_LOGIN_REFRESH.search(html).group("url")

            # Round 3
            html = self._get_html(refresh_url, postback_url)
            _call_hook(hook, 75)

            action = get_element_attr_by_id(html, "fmHF", "action")
            params = urllib.urlencode(generate_form_params(html, "fmHF"))

            # Round 4
            html = self._get_html(action, refresh_url, params)
            _call_hook(hook, 100)
            
            self._cookie_jar.save()

            self._parse_root_page(html)
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)
        except IOError, err:
            raise SkyDriveApiOSError(err)
        else:
            self._auth = True

    def _parse_root_page(self, html):
        """Parse the home page of skydrive."""
        try:
            begin_str = '<div id="documentsTileView" class="tvContainer">'
            begin = html.find(begin_str)

            m_url = _RE_INDEX_URL.search(html, begin)
            while m_url:
                #m_name = _RE_INDEX_NAME.search(html, m_url.end())

                full_url = unescape(m_url.group('href'))
                split_res = urlsplit(full_url)

                base_url = split_res[0] + "://" + split_res[1]
                name = full_url[full_url.rfind('/') + 1:]

                curr_dir = SkyDriveDir(name)
                curr_dir.set_base_url(base_url)

                if base_url.endswith(".skydrive.live.com"):
                    self._root_dir.set_base_url(base_url)

                self._root_dir.add_child(curr_dir)
                m_url = _RE_INDEX_URL.search(html, m_url.end())
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

    def _fill_dir(self, dest):
        """Fetch and parse the content of directory 'dest'"""
        dest.clear()

        try:
            html = self._get_html(dest.get_browse_url())

            dest.urls["downloadFolder"] = get_element_attr_by_id(html, 
                    "downloadFolder", "href")

            begin = html.find('<div id="dvContainer">')
            m_row = _RE_BROWSE_ROW.search(html, begin)
            while m_row:
                m_name = _RE_BROWSE_NAME.search(html, m_row.end())
                m_person = _RE_BROWSE_PERSON.search(html, m_name.end())
                m_time = _RE_BROWSE_TIME.search(html, m_person.end())
                m_del = _RE_BROWSE_DEL.search(html, m_time.end())
                
                name = unescape(m_name.group('name'))
                #url = unescape(m_name.group('href'))
                owner = unescape(m_person.group('person'))
                mtime = unescape(m_time.group('time'))
                #sid = unescape(m_del.group('id'))

                if m_row.group('id').startswith("dvFile"):
                    item = SkyDriveFile(name, owner, mtime, dest)
                else:
                    item = SkyDriveDir(name, owner, mtime, dest)

                m_url = _RE_BROWSE_URL.search(html, m_time.end(),
                        m_del.start())
                while m_url:
                    item.urls[m_url.group('id')] = unescape(m_url.group('href'))
                    m_url = _RE_BROWSE_URL.search(html, 
                            m_url.end(), m_del.start())

                dest.add_child(item)

                m_row = _RE_BROWSE_ROW.search(html, m_del.end())
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

        dest.timestamp = time.time()


    def upload(self, path, files, hook = None):
        """Upload files to skydrive."""
        assert len(files) <= 5

        dest = self.find_dir(path)

        url =  dest.get_base_url() + "/upload.aspx"
        url += urllib.quote(dest.get_abs_path()) + "?nonrich=1"

        opener = urllib2.build_opener(
                            urllib2.HTTPCookieProcessor(self._cookie_jar),
                            urllib2.HTTPRedirectHandler,
                            MultipartPostHandler)
        
        try:
            sock = opener.open(url)
            html = sock.read()

            params = generate_form_params(html, "aspnetForm")
            
            for i in range(len(files)):
                params["fileUpload%d" % (i + 1)] = open(files[i], "rb")

            sock = opener.open(url, params)
            html = sock.read()
            dest.timestamp = 0
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)


    def _download(self, path, out, hook = None, down_zip = False):
        """Download file or directory (as zip file)."""
        item = self.find_item(path)

        if isdir(item) and not down_zip:
            raise SkyDriveApiError('"%s" is a directory.' % item.get_abs_path())

        if isfile(item) and down_zip:
            raise SkyDriveApiError('"%s" is a file. Can\'t download as zip. ' %
                    item.get_abs_path())

        try:
            if down_zip and "downloadFolder" in item.urls:
                url = item.urls["downloadFolder"]
            elif down_zip:
                url = _get_download_folder_url(item)
            else:
                url = item.urls["download"]

            sock = self._opener.open(url)

            total_size = 0
            if 'Content-length' in sock.info():
                total_size = int(sock.info()[('Content-Length')])

            begin_time = time.time()
            buf_size = 1024 * 16
            read_size = 0
            buf = sock.read(buf_size)
            while len(buf) > 0:
                out.write(buf)
                read_size += len(buf)

                if hook and total_size > 0: 
                    # KB/S
                    speed = read_size / (time.time() + 1 - begin_time) / 1024
                    hook(100 * read_size/total_size, "%.2f KB/S" % speed)
                elif hook and total_size == 0:
                    hook(-1, str(read_size/1000) + "KB downloaded.")

                buf = sock.read(buf_size)
        except KeyError, err:
            # FIXME
            raise SkyDriveApiInternalError("Parser error. No download url.")
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

    def download(self, path, out, hook = None):
        """Download file."""
        self._download(path, out, hook, False)

    def download_zip(self, path, out, hook = None):
        """Download directory as zip file."""
        self._download(path, out, hook, True)

    def find_item(self, item_path):
        """Find the item at item_path (absolute or relative)."""
        err_msg = 'No such file or directory "%s"' % item_path

        if item_path.startswith('/'):
            dest = self._root_dir
        else:
            dest = self._cwd

        for name in item_path.split('/'):
            if name == '':
                continue
            elif name == '.':
                continue
            elif name == '..':
                dest = dest.parent
            elif isdir(dest):
                if dest.timestamp == 0: 
                    self._fill_dir(dest)
                dest = dest.find_child(name)
            else:
                raise SkyDriveApiNotExistError(err_msg)

            if dest is None: 
                raise SkyDriveApiNotExistError(err_msg)

        return dest

    def find_dir(self, path):
        """Find the directory at path"""

        err_msg = 'No directory "%s"' % path

        dest = self.find_item(path)
        if not isdir(dest):
            raise SkyDriveApiNotExistError(err_msg)

        #if dest.timestamp == 0: self._fill_dir(dest)
        return dest 

    def listdir(self, path):
        """Return a list of SkyDriveItem in the directory.

        The following exceptions will be raised if something wrong:
        SkyDriveApiNoDirError -- path doesn't exist
        SkyDriveApiNetworkError -- Networking failure
        SkyDriveApiInternalError -- internal parsing error

        """
        dest = self.find_dir(path)

        if dest and isdir(dest): 
            if dest.timestamp == 0: 
                self._fill_dir(dest)
            return dest.get_subitems()
        elif dest:
            return [dest]
        else:
            return dest

    def chdir(self, path):
        """Change current working directory to path.

        path can be absolute or relative path.

        """
        dest = self.find_dir(path)

        if dest is None:
            raise SkyDriveApiNoDirError('No directory "%s"' % path)
        else:
            self._cwd = dest

    def getcwd(self):
        """Return current working directory (absolute path)."""
        return self._cwd.get_abs_path()

    def mkdir(self, dpath):
        """Make directory dpath.

        dpath is a absolute or relative path seperated by "/". If "/" is found 
        in dpath, dpath will be seperated by the last "/". The method will 
        create a directory named as the second part under the first part. 
        SkyDriveApiNoDirError will be raised if no directory's path is the same
        as the first part.

        If "/" isn't found in dpath, a directory named dpath will be created 
        under current working directory.

        SkyDriveApiNetworkError or SkyDriveApiInternalError will be raised when
        the operation fails.

        """
        pos = dpath.rfind('/')
        if pos > 0:
            dest = self.find_dir(dpath[:pos])
            name = dpath[(pos + 1):]
        else:
            dest = self._cwd
            name = dpath

        abs_path = urllib.quote(dest.get_abs_path())
        if abs_path == "/":
            url = dest.get_base_url() + "/newlivefolder.aspx?ref=5"
        else:
            url = dest.get_base_url() + '/newfolder.aspx' + abs_path + '?ref=1'

        try:
            html = self._get_html(url)

            begin = html.find('<form name="aspnetForm"') 
            params = generate_form_params(html[begin:], 'aspnetForm')
            if "itemName" in params:
                params["itemName"] = name
            elif "LiveFolderName" in params:
                params["LiveFolderName"] = name

            params = urllib.urlencode(params)

            html = self._get_html(url, "", params)

            re_err = re.compile(r'<span class="errorText">(?P<errmsg>.*?)'
                                r'</span>')
            m_err = re_err.search(html)
            if m_err:
                raise SkyDriveApiError(unescape(m_err.group('errmsg')))
            else:
                # FIXME: if dest is "/", all the contents can't be seen 
                # in this session
                dest.timestamp = 0
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

    def delete(self, path):
        """Remove file or directory whose path is path (absolute or relative)

        If given path doesn't exists, SkyDriveApiNotExistError will be raised.
        If the operation fails, SkyDriveApiNetworkError or SkyDriveInternalError
        will be raised.

        True will be returned if all operations are done successfully.

        """
        item = self.find_item(path)
        try:
            url = item.get_base_url() + "/self.aspx"
            url += urllib.quote(item.get_abs_path())

            html = self._get_html(url, item.parent.get_browse_url())

            params = generate_form_params(html, "aspnetForm")
            params["postVerb"] = "deleteItem"
            #params["postVerbData"] = item.sid
            params = urllib.urlencode(params)

            html = self._get_html(url, "", params)

            re_err = re.compile(r'<span class="errorText">(?P<errmsg>.*?)'
                                r'</span>')
            m_err = re_err.search(html)
            if m_err:
                raise SkyDriveApiError(unescape(m_err.group('errmsg')))
            else:
                item.parent.del_child(item)
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

    def rename(self, path, new_name):
        """Rename the file "path" to new_name

        """
        item = self.find_item(path)
        try:
            url = item.get_base_url() + "/rename.aspx"
            url += urllib.quote(item.get_abs_path())
            url += "?ref=11"

            html = self._get_html(url)

            params = generate_form_params(html, "aspnetForm")
            params["itemName"] = new_name
            params = urllib.urlencode(params)

            html = self._get_html(url, "", params)
    
            re_err = re.compile(r'<span class="itemNameErrorText">'
                                r'(?P<errmsg>.*?)</span>')
            m_err = re_err.search(html)
            if m_err:
                raise SkyDriveApiError(unescape(m_err.group('errmsg')))
            else:
                item.name = new_name
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

    def _movecopy(self, path, to_path, is_copy = False):
        """Move file/directory or is_copy file."""
        item = self.find_item(path)
        dest = self.find_dir(to_path)
        
        operation = "move"
        if is_copy: 
            operation = "is_copy"

        # directories under "/" can't be moved.
        if operation not in item.urls:
            raise SkyDriveApiError('Can\'t %s "%s".' % (operation, path))

        try:
            split_res = urlsplit(item.urls[operation])
            url = dest.get_base_url() + "/movecopy.aspx"
            url += urllib.quote(dest.get_abs_path())
            url += "?" + split_res[3]
            
            html = self._get_html(url, item.urls[operation])
            params = generate_form_params(html, "aspnetForm")
            params["linkClicked"] = "true"
            params = urllib.urlencode(params)

            html = self._get_html(url, url, params)
            if is_copy:
                # FIXME: there will be some problem if we continue to operate 
                # the file copied as the sid/urls are not updated.
                dest.add_child(copy.copy(item))
            else:
                item.parent.del_child(item)
                dest.add_child(item)
        except urllib2.URLError, err:
            raise SkyDriveApiNetworkError(err)
        except AttributeError, err:
            raise SkyDriveApiInternalError(err)

    def move(self, path, to_path):
        """Move file or directory from path to to_path"""
        self._movecopy(path, to_path, False)

    def copy(self, path, to_path):
        """Copy file from path to directory to_path."""
        self._movecopy(path, to_path, True)


if __name__ == "__main__":
    pass
